package de.jmda.core.mproc.task;

import static de.jmda.core.mproc.ProcessingUtilities.getPackageOf;
import static de.jmda.core.mproc.ProcessingUtilities.getRoundEnvironment;
import static de.jmda.core.mproc.ProcessingUtilities.normalizeBinaryClassName;
import static de.jmda.core.mproc.ProcessingUtilities.withEnclosedTypeElements;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;

import de.jmda.core.MarkerAnnotationType;
import de.jmda.core.mproc.ProcessingUtilities;

/**
 * Base class for {@link Task}s that are targeted to the {@link TypeElement}s
 * specified by {@link #packages} (all type elements in {@linkplain #packages})
 * plus those type elements specified by {@link #types}. The type elements will
 * be available inside the {@link #execute()} method via {@link
 * #getTypeElements()}.
 *
 * @author roger@jmda.de
 */
public abstract class AbstractTypeElementsTask extends AbstractTask
{
	private Set<Package> packages;
	private Set<? extends Class<?>> types;

	public AbstractTypeElementsTask(
			Set<Package> packages, Set<? extends Class<?>> types)
  {
	  super();

	  if (packages == null)
	  {
	  	this.packages = new HashSet<>();
	  }
	  else
	  {
		  this.packages = packages;
	  }

	  if (types == null)
	  {
	  	this.types = new HashSet<>();
	  }
	  else
	  {
		  this.types = types;
	  }
  }

	public Set<Package> getPackages()
	{
		return packages;
	}

	public Set<? extends Class<?>> getTypes()
	{
		return types;
	}

	/**
	 * Callback method that can be used inside {@link #execute()} to retrieve the
	 * type elements as defined by {@link #packages} and {@link #types}.
	 *
	 * @return Those type elements from {@link
	 *         ProcessingUtilities#getRoundEnvironment()} that are
	 *         <ul>
	 *           <li>from one of the {@link #packages} or</li>
	 *           <li>correspond to one of the types in {@link #types}.</li>
	 *         </ul>
	 *         In other words: Those type elements that correspond to all the
	 *         types in all the {@link #packages} plus the type elements that
	 *         correspond to {@link #types}.
	 */
	protected Set<TypeElement> getTypeElements()
	{
		Set<TypeElement> result = new HashSet<>();
		
		Set<TypeElement> allAnnotatedTypeElements =
				ProcessingUtilities.getTypeElements(
						getRoundEnvironment().getElementsAnnotatedWith(
								MarkerAnnotationType.class));

		Set<String> packageNames = asPackageNameSet(packages);

		for (TypeElement annotatedTypeElement : allAnnotatedTypeElements)
		{
			PackageElement packageOfAnnotatedElement =
					getPackageOf(annotatedTypeElement);

			Set<TypeElement> typesInPackageOfAnnotatedElement =
					ProcessingUtilities.getTypeElements(
							packageOfAnnotatedElement.getEnclosedElements());

			if (packageNames.contains(
			    packageOfAnnotatedElement.getQualifiedName().toString()))
			{
				// the package of the annotated element belongs to the packages in the
				// parameters, all types in the package have to be added to result
				result.addAll(typesInPackageOfAnnotatedElement);
			}
			else
			{
				// the package of the annotated element does not belong to the packages
				// in the parameters, so the types in the parameter have to be searched
				// in the types of the package
				// search will include enclosed type elements
				result.addAll(
						findTypesInPackageTypes(
								types,
								withEnclosedTypeElements(typesInPackageOfAnnotatedElement)));
			}
		}
		
		return result;
	}

	private Set<String> asPackageNameSet(Set<Package> packages)
  {
		Set<String> result = new HashSet<>();

		for (Package package_ : packages)
		{
			result.add(package_.getName());
		}
		
		return result;
  }

	private Set<TypeElement> findTypesInPackageTypes(
      Set<? extends Class<?>> types, Set<TypeElement> packageTypes)
  {
		Set<TypeElement> result = new HashSet<>();
		
		Map<String, TypeElement> packageTypeElementsByQualifiedName =
				getTypeElementsByQualifiedName(packageTypes);

		for (Class<?> type : types)
		{
			String normalizedQualifiedTypeName = normalizeBinaryClassName(type);

			if (packageTypeElementsByQualifiedName.keySet()
			                                      .contains(
			                                      		normalizedQualifiedTypeName))
			{
				result.add(
						packageTypeElementsByQualifiedName.get(normalizedQualifiedTypeName));
			}
		}

		return result;
  }

	/**
	 * @param types
	 * @return map with all type elements plus their recursively enclosed type
	 *         elements by their qualified name
	 */
	private Map<String, TypeElement> getTypeElementsByQualifiedName(
			Set<TypeElement> types)
  {
		Map<String, TypeElement> result = new HashMap<>();
		
		for (TypeElement type : types)
		{
			result.put(type.getQualifiedName().toString(), type);

			// recursion
			result.putAll(
					getTypeElementsByQualifiedName(
							ProcessingUtilities.getTypeElements(type.getEnclosedElements())));
		}

		return result;
  }
}